﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace ServerNodeLib
{
	/// <summary>
	/// This is the server component that uses a persistent data source,
	/// in this case the IDataAdapter interfaces.
	/// 
	/// This class is conceptually based on the round robin server
	/// </summary>
	[ServiceBehaviorAttribute(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
	public class PersistentSchedulerServer : ISchedulerServer
	{
		#region data members
		private int idleNodes;
		public int IdleNodes
		{
			get
			{
				return this.idleNodes;
			}
		}
		private int totalNodes;
		public int TotalNodes
		{
			get
			{
				return this.totalNodes;
			}
		}
		public DataTable Nodes
		{
			get
			{
				return (DataTable)dataAdapter.Nodes;
			}
		}


		private int queuedBatches;
		public int QueuedBatches
		{
			get
			{
				return this.queuedBatches;
			}
		}
		private int totalBatches;
		public int TotalBatches
		{
			get
			{
				return totalBatches;
			}
		}
		public DataTable Batches
		{
			get
			{
				return (DataTable)dataAdapter.Batches;
			}
		}

		private DataAdapter.XmlDataAdapter dataAdapter;

		private System.Security.Cryptography.MD5 md5;
		private System.Threading.Thread updateThread;
		private ServiceHost SchedulerServerHost;
		private DateTime lastSeen;
		/// <summary>
		/// Delegate for notifying the server that we've completed a refresh loop
		/// </summary>
		public Action OnStateChanged;
		public Action OnServerToggled;
		#endregion

		#region class and thread management
		public PersistentSchedulerServer()
		{
			queuedBatches = 0;
			totalBatches = 0;
			idleNodes = 0;
			totalNodes = 0;
			md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
			dataAdapter = new ServerNodeLib.DataAdapter.XmlDataAdapter();

			ServerNodeSettings.Default.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Default_PropertyChanged);
			lastSeen = DateTime.Now;
		}

		public void Start()
		{
			try
			{
				CommonLib.Tracer.Start();
				if (SchedulerServerHost != null)
				{
					SchedulerServerHost.Close();
				}
				SchedulerServerHost = new ServiceHost(this, new Uri(ServerNodeSettings.Default.SchedulerServerWcfUrl));
				NetTcpBinding tcpBinding = new NetTcpBinding(SecurityMode.None);
				//tcpBinding.MaxConnections = 50;

				SchedulerServerHost.AddServiceEndpoint(
					typeof(ISchedulerServer),
					tcpBinding,
					String.Empty
				);

				ServiceMetadataBehavior smb = SchedulerServerHost.Description.Behaviors.Find<ServiceMetadataBehavior>();
				// If not, add one
				if (smb == null)
					smb = new ServiceMetadataBehavior();

				smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
				//smb.HttpGetEnabled = true;

				SchedulerServerHost.Description.Behaviors.Add(smb);
				SchedulerServerHost.AddServiceEndpoint(
				  ServiceMetadataBehavior.MexContractName,
				  MetadataExchangeBindings.CreateMexTcpBinding(),
				  "mex"
				);

				SchedulerServerHost.Open();
				if (updateThread != null && updateThread.IsAlive)
					updateThread.Abort();
				updateThread = new System.Threading.Thread(this.RefreshLoop);
				updateThread.Start();
				if (OnServerToggled != null)
					this.OnServerToggled.Invoke();
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
				this.Stop();
			}
		}

		public void Stop()
		{
			try
			{
				if (updateThread != null)
					updateThread.Abort();
				if (SchedulerServerHost != null)
					SchedulerServerHost.Close();
				if (OnServerToggled != null)
					this.OnServerToggled.Invoke();
				CommonLib.Tracer.Stop();
			}
			catch (Exception ex)
			{
				CommonLib.Tracer.ReportError("Generic error", ex);
			}
		}

		public bool IsRunning()
		{
			if (updateThread == null || SchedulerServerHost == null)
				return false;
			if (updateThread.IsAlive == false)
				return false;
			if (SchedulerServerHost.State != CommunicationState.Opened)
				return false;
			return true;
		}

		#endregion

		#region ISchedulerServer Members
		public Guid AnnounceMyself(string name)
		{
			if (String.IsNullOrEmpty(name))
				return Guid.Empty;
			try
			{
				// If the client calls announce, we know that it needs a new id
				/*
				foreach (Node n in dataAdapter.GetNodes())
				{
					if (n.NodeName.Equals(name))
						return n.NodeId;
				}
				*/
				Node newNode = new Node();
				if (dataAdapter.SaveNode(newNode))
				{
					CommonLib.Tracer.ReportInformation("Announce from " + name + " with new ID " + newNode.NodeId);
					return newNode.NodeId;
				}
				else
				{
					return Guid.Empty;
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
				return Guid.Empty;
			}
		}

		public bool UnAnnounceMyself(Guid nodeId)
		{
			try
			{
				Batch b = dataAdapter.GetBatchOnNode(nodeId);
				if (b != null)
				{
					ResetBatch(b);
				}
				CommonLib.Tracer.ReportInformation("UnAnnounce from " + nodeId);
				return dataAdapter.DeleteNode(nodeId);
			}
			catch (Exception ex)
			{
				CommonLib.Tracer.ReportError("Generic exception", ex);
				return false;
			}
		}

		public ActionRequest SetStatus(Guid nodeId, NodeStatus newStatus)
		{
			try
			{
				if (newStatus == null)
					return null;
				ActionRequest responseRequest = new ActionRequest(ActionRequestType.AnnounceNode);
				// don't completely trust the client code
				newStatus.NodeId = nodeId;
				Node n = dataAdapter.GetNode(nodeId);
				if (n == null)
				{
					return new ActionRequest(ActionRequestType.AnnounceNode);
				}

				Batch b = dataAdapter.GetBatchOnNode(newStatus.NodeId);
				if (newStatus.BatchId.Equals(Guid.Empty))
				{
					// check that there isn't a batch allocated to this node

					// this code breaks the case that a node has finished
					// a job and the result is in its communication queue
					/*
					if (b != null)
					{
						ResetBatch(b);
						dataAdapter.SaveBatch(b);
					}
					*/
				}
				else
				{
					// check that the node is doing the same job as we think it is
					if(b == null || b.BatchId.Equals(newStatus.BatchId) == false)
						responseRequest = new ActionRequest(ActionRequestType.KillJob);
				}

				// update status accordingly
				n.AcceptNewStatus(newStatus);
				n.LastSeen = DateTime.Now; // unsure if set elsewhere

				if (n.ActionRequests.Count > 0)
				{
					responseRequest = n.ActionRequests[0];
					n.ActionRequests.RemoveAt(0);
				}
				else
				{
					responseRequest = new ActionRequest(ActionRequestType.NoAction);
				}

				if (responseRequest.ActionType == ActionRequestType.NoAction && newStatus.State == NodeState.Idle)
				{
					// check if there is another batch to download
					// now the client can choose!
					if (queuedBatches > 0 && newStatus.AcceptNewJobs)
						responseRequest.ActionType = ActionRequestType.GetJob;
				}
				dataAdapter.SaveNode(n);
				return responseRequest;
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
				return new ActionRequest(ActionRequestType.Error);
			}
		}

		public NodeStatus GetStatus(Guid clientGuid)
		{
			Node n =dataAdapter.GetNode(clientGuid);
			if (n != null)
			{
				return n.GetNodeStatus();
			}
			else
			{
				return null;
			}
		}

		public List<Guid> GetAllNodes()
		{
			//throw new NotImplementedException("I need a better sql statement for this, and I haven't found a reason to use it yet");
			return new List<Guid>();
		}

		public BatchInfo GetAvailableBatchJob(Guid clientGuid)
		{
			try
			{
				// first, see if I know the client
				Node n = dataAdapter.GetNode(clientGuid);
				if (n == null || n.State != NodeState.Idle || n.AcceptNewJobs == false)
				{
					// wait until the next status update
					return null;
				}

				List<Batch> nextBatch = dataAdapter.GetBatches(BatchStatus.NotStarted, 1);
				if (nextBatch.Count == 0)
				{
					return null;
				}
				else
				{
					nextBatch[0].NodeId = n.NodeId;
					nextBatch[0].NodeBegin = DateTime.Now;
					nextBatch[0].Status = BatchStatus.InProgress;
					// let the node self-report a change in status
					dataAdapter.SaveBatch(nextBatch[0]);

					// don't change the node, since it's the status update
					// should take care of that
					return BuildBatchInfo(nextBatch[0]);
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
			}
			return null;
		}


		public byte[] GetResourceChunk(Guid batchId, string destName, int chunkNum)
		{
			try
			{
				BatchResource resource = dataAdapter.GetBatchResource(batchId, destName);
				if (resource == null || System.IO.File.Exists(resource.ServerPath) == false)
					return null;// throw new System.IO.FileNotFoundException(ExceptionMessages.ResourceFileNotFound, destName);
				using (System.IO.FileStream fs = new System.IO.FileStream(resource.ServerPath, System.IO.FileMode.Open))
				{
					int chunks = (int)(fs.Length / ServerNodeSettings.Default.TransferChunkSize);
					if (chunkNum > chunks || chunkNum < 0)
						return null;
					//throw new ArgumentException(String.Format(ExceptionMessages.ChunkNumInvalid, 0, chunks, chunkNum));

					int offset = chunkNum * ServerNodeSettings.Default.TransferChunkSize;
					int dataSize = 0;
					if (chunkNum == chunks)
					{
						dataSize = (int)fs.Length - offset;
					}
					else
					{
						dataSize = ServerNodeSettings.Default.TransferChunkSize;
					}

					byte[] retData = new byte[dataSize];

					fs.Seek(offset, System.IO.SeekOrigin.Begin);
					int readData = fs.Read(retData, 0, retData.Length);
					if (readData != dataSize)
						return null;
					//throw new System.IO.InvalidDataException(String.Format(ExceptionMessages.InsufficientDataRead, destName, readData, dataSize));

					return retData;
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
				return null;
			}
		}

		public Guid PostResult(Guid batchId, ResultInfo result)
		{
			try
			{
				Batch theBatch = dataAdapter.GetBatch(batchId);
				if (theBatch == null)
				{
					theBatch = new Batch();
				}
				if (result == null)
					return Guid.Empty;

				Guid postResultGuid = Guid.NewGuid();

				theBatch.NodeEnd = DateTime.Now;
				theBatch.Result = result;

				BatchGroup group = dataAdapter.GetBatchGroup(theBatch.BatchGroup);
				if (group == null)
				{
					group = new BatchGroup();
					theBatch.BatchGroup = group.GroupId;
					group.OutputDirectory = System.IO.Path.Combine("unknown", System.IO.Path.GetRandomFileName());
				}
				Node node = dataAdapter.GetNode(theBatch.NodeId);
				if (node == null)
				{
					node = new Node();
				}

				String destPath = System.IO.Path.Combine(ServerNodeSettings.Default.ResultResourceDirectory, group.OutputDirectory);
					if (System.IO.Directory.Exists(destPath) == false)
						System.IO.Directory.CreateDirectory(destPath);

				// check that the return resources were sent properly
				if (result.IsSuccess)
				{
					foreach (BatchResource b in theBatch.ReturnResources)
					{
						String filename = System.IO.Path.GetFileName(b.ServerPath);
						if (System.IO.File.Exists(System.IO.Path.Combine(destPath, filename)) == false)
						{
							node.ActionRequests.Add(new ActionRequest(ActionRequestType.SendResultQueue, String.Empty, theBatch.BatchId.ToString()));
							return Guid.Empty;
						}
					}
				}

				if (result.IsSuccess)
				{
					String resultFileName = theBatch.LogFilenameFormat;
					if(System.IO.Path.GetExtension(resultFileName).Equals(".xml") == false)
						resultFileName += ".xml";
					
					String serializedPath = System.IO.Path.Combine(destPath, resultFileName);
					if (System.IO.File.Exists(serializedPath))
					{
						serializedPath = System.IO.Path.Combine(destPath, Guid.NewGuid().ToString() + ".xml");
					}

					using (System.IO.StreamWriter writer = new System.IO.StreamWriter(serializedPath))
					{
						System.Xml.Serialization.XmlSerializer s = new System.Xml.Serialization.XmlSerializer(typeof(Batch));
						s.Serialize(writer, theBatch);
						writer.Close();
					}

					serializedPath = System.IO.Path.Combine(destPath, ServerNodeSettings.Default.GroupInfoFileName);

					group.CompleteBatches++;
					group.RemainingBatches--;
					if (group.TotalBatches == group.CompleteBatches)
					{
						group.EndTime = DateTime.Now;
						dataAdapter.DeleteBatchGroup(group.GroupId);
					}
					else
					{
						dataAdapter.SaveBatchGroup(group);
					}
					System.Xml.Serialization.XmlSerializer xsg = new System.Xml.Serialization.XmlSerializer(typeof(BatchGroup));
					using (System.IO.StreamWriter w = new System.IO.StreamWriter(serializedPath))
					{
						xsg.Serialize(w, group);
						w.Close();
					}

					theBatch.NodeId = Guid.Empty;
					theBatch.Status = BatchStatus.Completed;
					dataAdapter.DeleteBatch(theBatch.BatchId);
				}
				else
				{
					CommonLib.Logger.Instance.Report(CommonLib.LogType.Event,
						this,
						String.Format(ExceptionMessages.BatchJobNotSuccess, theBatch.BatchId, theBatch.NodeId, result.SummaryMessage));

					ResetBatch(theBatch);
					dataAdapter.SaveBatch(theBatch);
				}

				return postResultGuid;
			}catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
			}
			return Guid.Empty;
		}

		public bool PostResourceChunk(Guid batchId, String clientPath, int chunkNum, byte[] data)
		{
			try
			{
				Batch b = dataAdapter.GetBatch(batchId);
				// we want to keep all results
				String serverPath = null;
				if (b == null)
				{
					serverPath = System.IO.Path.GetRandomFileName();
					b = new Batch();
					b.BatchGroup = Guid.NewGuid();
					b.BatchId = Guid.Empty;
				}
				else
				{
					foreach (BatchResource res in b.ReturnResources)
					{
						if (res.ClientPath.Equals(clientPath))
							serverPath = res.ServerPath;
					}
				}
				if (String.IsNullOrEmpty(serverPath))
				{
					serverPath = batchId.ToString() + " " + System.IO.Path.GetFileName(clientPath);
				}

				BatchGroup group = dataAdapter.GetBatchGroup(b.BatchGroup);
				String destPath = System.IO.Path.Combine(ServerNodeSettings.Default.ResultResourceDirectory, group.OutputDirectory);
				if (System.IO.Directory.Exists(destPath) == false)
					System.IO.Directory.CreateDirectory(destPath);
				destPath = System.IO.Path.Combine(destPath, System.IO.Path.GetFileName(serverPath));

				System.IO.FileMode mode = System.IO.FileMode.Create;
				if (chunkNum != 0)
					mode = System.IO.FileMode.Append;

				using (System.IO.FileStream fs = new System.IO.FileStream(destPath, mode))
				{
					fs.Write(data, 0, data.Length);
					fs.Close();
				}
				return true;
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
			}
			return false;
		}

		#endregion

		#region GUI interface points

		/// <summary>
		/// Removes the node from the data store
		/// </summary>
		/// <param name="nodeId"></param>
		/// <returns></returns>
		public bool DropNode(Guid nodeId)
		{
			return dataAdapter.DeleteNode(nodeId);
		}

		/// <summary>
		/// Tells the node to kill its current job.
		/// A false means either the node does not exist or it is not busy
		/// </summary>
		/// <param name="nodeId"></param>
		/// <returns>True if the request was placed successfully</returns>
		public bool ResetNode(Guid nodeId)
		{
			Node n = dataAdapter.GetNode(nodeId);
			if (n == null)
			{
				return false;
			}
			else
			{
				if (n.State == NodeState.Running)
				{
					n.ActionRequests.Add(new ActionRequest(ActionRequestType.KillJob));
					return dataAdapter.SaveNode(n);
				}
				else
				{
					return false;
				}
			}
		}

		/// <summary>
		/// Removes the batch from the data store
		/// 
		/// This method does not touch the nodes because during the next
		/// status report the server should identify the unexpected state
		/// and recover
		/// </summary>
		/// <param name="batchId"></param>
		/// <returns></returns>
		public bool DropBatch(Guid batchId)
		{
			return dataAdapter.DeleteBatch(batchId);
		}

		/// <summary>
		/// Resets the state of the given batch so that an idle node
		/// will pick it up
		/// 
		/// This method does not affect nodes because the change
		/// in state should be caught by the node status report
		/// </summary>
		/// <param name="batchId"></param>
		/// <returns></returns>
		public bool ResetBatch(Guid batchId)
		{
			Batch b = dataAdapter.GetBatch(batchId);
			if (b == null)
			{
				return false;
			}
			else
			{
				ResetBatch(b);
				return dataAdapter.SaveBatch(b);
			}
		}

		public Batch GetBatch(Guid batchId)
		{
			return dataAdapter.GetBatch(batchId);
		}

		public Node GetNode(Guid nodeId)
		{
			return dataAdapter.GetNode(nodeId);
		}

		/// <summary>
		/// Enqueues an action to be delivered to the node next status update
		/// </summary>
		/// <param name="nodeId">The node to request</param>
		/// <param name="req">The specific request</param>
		public void EnqueueActionRequest(Guid nodeId, ActionRequest req)
		{
			if (req == null)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ExceptionMessages.ArgumentNull + "req");
				return;
			}
			Node n = dataAdapter.GetNode(nodeId);
			if (n == null)
				return;
			n.ActionRequests.Add(req);
			dataAdapter.SaveNode(n);
		}

		#endregion

		#region helper methods
		/// <summary>
		/// Background thread updates the state of the server.
		/// I do this periodically because it's just not important
		/// enough to do continually. Plus, large node numbers make event
		/// handlers a little heavy.
		/// </summary>
		private void RefreshLoop()
		{
			while (true)
			{
				try
				{
					CheckNodeTimeouts();
					CountBatches();
					CountNodes();
					if (OnStateChanged != null)
						OnStateChanged.Invoke();
					//SaveState();
					System.Threading.Thread.Sleep(ServerNodeSettings.Default.RefreshLoopSleep);
				}
				catch (System.Threading.ThreadAbortException)
				{
					return;
				}
				catch (Exception ex)
				{
					CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
					System.Threading.Thread.Sleep(ServerNodeSettings.Default.RefreshLoopSleep);
				}
				finally
				{

				}
			}
		}

		/// <summary>
		/// Checks nodes for timeouts!
		/// Updates batches object if a timeout is detected.
		/// </summary>
		private void CheckNodeTimeouts()
		{
				DateTime now = DateTime.Now;
				//if (now.Subtract(lastSeen).CompareTo(ServerNodeSettings.Default.NodeTimeout) > 0) ;
				foreach (Node n in dataAdapter.GetNodes(now.Subtract(ServerNodeSettings.Default.NodeTimeout)))
				{
					dataAdapter.DeleteNode(n.NodeId);
				}
		}

		private void CountBatches()
		{
			totalBatches = dataAdapter.NumberBatches();
			queuedBatches = dataAdapter.NumberBatches(BatchStatus.NotStarted);
		}

		private void CountNodes()
		{
			totalNodes = dataAdapter.NumberNodes();
			idleNodes = dataAdapter.NumberNodes(NodeState.Idle);
		}

		/// <summary>
		/// Resets the given batch
		/// under the hood it's an alias for the data adapter function
		/// Originally I didn't place this function at the correct level,
		/// and I'm "fixing" it now with an alias.
		/// </summary>
		/// <param name="b"></param>
		private void ResetBatch(Batch b)
		{
			DataAdapter.XmlDataAdapter.ResetBatch(b);
		}

		/// <summary>
		/// Given a server-defined Batch, this builds a BatchInfo object for
		/// a node to consume.
		/// 
		/// I reocmmend doing this each time a node requests a new batch.
		/// The reason is that hashes may change or files may disappear when you
		/// least expect it.
		/// </summary>
		/// <param name="b">The batch object</param>
		/// <returns></returns>
		private BatchInfo BuildBatchInfo(Batch b)
		{
			BatchInfo binfo = new BatchInfo();
			binfo.BatchId = b.BatchId;
			binfo.BatchResources = new List<ResourceInfo>();
			foreach (BatchResource res in b.BatchResources)
			{
				if (System.IO.File.Exists(res.ServerPath))
				{
					ResourceInfo info = new ResourceInfo();
					info.DestName = res.ClientPath;
					info.TransferChunkSize = ServerNodeSettings.Default.TransferChunkSize;
					using (System.IO.FileStream fs = new System.IO.FileStream(res.ServerPath, System.IO.FileMode.Open))
					{
						byte[] hash = md5.ComputeHash(fs);
						info.Md5Hash = Convert.ToBase64String(hash);
						info.DestSize = fs.Length;
					}
					binfo.BatchResources.Add(info);
				}
			}
			binfo.Commands = new List<CommandInfo>();
			foreach (BatchCommand com in b.BatchCommands)
			{
				CommandInfo info = new CommandInfo();
				info.Arguments = com.Arguments;
				info.BreakOnException = com.BreakOnException;
				info.CaptureOutput = com.CaptureOutput;
				info.Command = com.Command;
				info.ErrorOnTimeOut = com.ErrorOnTimeOut;
				info.MaxTimeSeconds = com.MaxTimeSeconds;
				info.MinTimeSeconds = com.MinTimeSeconds;
				info.Path = com.Path;
				binfo.Commands.Add(info);
			}
			binfo.ReturnResources = new List<ResourceInfo>();
			foreach (BatchResource res in b.ReturnResources)
			{
				ResourceInfo info = new ResourceInfo();
				info.DestName = res.ClientPath;
				//info.BatchId = b.BatchId;
				binfo.ReturnResources.Add(info);
				info.TransferChunkSize = ServerNodeSettings.Default.TransferChunkSize;
			}

			return binfo;
		}

		public ServerNodeSettings Settings
		{
			get
			{
				return ServerNodeSettings.Default;
			}
		}

		void Default_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
		{
			ServerNodeSettings.Default.Save();
		}

		#endregion

		#region batch template

		/// <summary>
		/// Given the template, this applies the pattern to the base batch
		/// and adds the individual batches to the queue.
		/// 
		/// Adding a template also creates a new "batchgroup" -- that is all
		/// of the resulting batches are "grouped" together so their results
		/// are stored in the same directory.
		/// </summary>
		/// <param name="template"></param>
		/// <param name="comment"></param>
		public void AddBatchTemplate(BatchTemplate template, String outdirPrefix, String comment)
		{
			if (comment == null)
				comment = String.Empty;
			
			BatchGroup bg = new BatchGroup();
			bg.Comment = comment;
			bg.StartTime = DateTime.Now;
			bg.OutputDirectory = bg.StartTime.ToLongDateString() + " " + bg.StartTime.Hour + " " + bg.StartTime.Minute + " " + bg.StartTime.Second;
			if (String.IsNullOrEmpty(outdirPrefix) == false)
			{
				bg.OutputDirectory = outdirPrefix + " " + bg.OutputDirectory;
			}
			template.BaseBatch.BatchGroup = bg.GroupId;

			// Long-term there is going to be a nice pattern model
			// until then, all I need is simple iteration
			Dictionary<String, String> variables = new Dictionary<string, string>();
			variables["i"] = "0";
			variables["j"] = "1";
			for (int configIndex = template.StartConfigIndex; configIndex < template.EndConfigIndex; configIndex++)
			{
				variables["i"] = configIndex.ToString();
				variables["j"] = (configIndex + 1).ToString();
				Batch newBatch = new Batch(template.BaseBatch, variables);
				newBatch.QueueAdd = DateTime.Now;
				newBatch.NodeId = Guid.Empty;
				newBatch.Status = BatchStatus.NotStarted;
				dataAdapter.SaveBatch(newBatch);
				//queuedBatches++;
				bg.TotalBatches++;
			}
			dataAdapter.SaveBatchGroup(bg);
		}
		#endregion
	}
}
